/**
 * Authors:
 * Vladimír Matěna
 * Jan Skalický
 */
package inireader;

import java.util.List;
import java.io.*;
import java.util.ArrayList;
import javax.xml.parsers.*;
import org.w3c.dom.*;
import org.xml.sax.SAXException;

/**
 * Main class represents whole configuration file.
 * Can be created with or without schema describing config structure.
 * When created without schema no structure validation is performed
 * and all elements are treated as strings. Class content can be
 * loaded from file or stream.
 */
public class Configurator
{
	/**
	 * Constants for key words in configuration file.
	 */
	static final String COMMENT = ";";
	static final String ELEMENT_SEPARATORS = ":,";
	static final String ELEMENT_ASSIGN = "=";
	static final String NEWLINE = System.getProperty("line.separator");
	static final String SECTION_LEFT_BRACKET = "[";
	static final String SECTION_RIGHT_BRACKET = "]";

	private List<Section> sections;
	Document schema;
	Validity validity;

	/**
	 * Creates configuration without schema.
	 */
	public Configurator()
	{
		sections = new ArrayList<Section>();
		validity = Validity.Relaxed;
	}

	/**
	 * Creates configuration with attached schema.
	 * @param iniSchema path to file describing schema
	 */
	public Configurator(String iniSchema, Validity validity)
			throws ParserConfigurationException, SAXException, IOException
	{
		this.validity = validity;
		sections = new ArrayList<Section>();

		// Load XML describing schema
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

		factory.setValidating(true);
		DocumentBuilder builder = factory.newDocumentBuilder();
		schema = builder.parse(iniSchema);
	}

	/**
	 * Load defaults for all property.
	 */
	private void loadDefaults()
	{
		org.w3c.dom.Element rootElement = schema.getDocumentElement();

		NodeList secNodes = rootElement.getElementsByTagName("section");

		for (int i = 0; i < secNodes.getLength(); i++)
		{
			org.w3c.dom.Element secElement = (org.w3c.dom.Element)secNodes.item(i);

			Identifier header = new Identifier(
					secElement.getElementsByTagName("header").item(0).getTextContent());

			Section currSection = getSection(header.getId());

			if (currSection == null)
			{
				boolean isSecMandatory = secElement.getAttribute("mandatory").matches("true");

				if (isSecMandatory)
				{
					throw new ValidatingException(String.format(
							"Mandatory section %s doesn't exist in ini file!",
							header.getId()));
				}

				Section newSec = new Section(header, this);
				// if true, exception is thrown, and this row is not executed
				newSec.setMandatory(false); 
				sections.add(newSec);

				currSection = newSec;
			}

			currSection.validateSection(secElement, validity);
		}

		if (validity == Validity.Strict)
		{
			checkAllSectionsValidated();
		}
	}

	/**
	 * Checks if all sections passed validation. If not throws exception.
	 * complaining about undefined sections.
	 */
	private void checkAllSectionsValidated()
	{
		for (Section section : sections)
		{
			if (!section.isValidated())
			{
				throw new ValidatingException(String.format(
						"There is unspecified section %s in ini file!",
						section.getHeader().getId()));
			}
		}
	}

	/**
	 * Validates all elements of all sections.
	 */
	void validateAllElements()
	{
		for (Section section : sections)
		{
			section.validateAllElements();
		}
	}

	/**
	 * Get newSection by name.
	 * @param header newSection header as String
	 */
	public Section getSection(String header)
	{
		for (Section section : sections)
		{
			if (header.equals(section.getHeader().getId()))
			{
				return section;
			}
		}

		return null;
	}

	/**
	 * Add newSection to configuration.
	 * @param newSection the section
	 */
	public void addSection(Section newSection)
	{
		if(validity == Validity.Strict)
		{
			throw new ValidatingException(String.format(
					"Cannot add Section \"%s\"" +
					" to configuration using strict schema!",
					newSection.getHeader().getId()));
		}

		if(newSection.configurator != this)
		{
			throw new ValidatingException(String.format(
					"Cannot add section \"%s\"" +
					" it belongs to another configuration",
					newSection.getHeader().getId()));
		}

		// check if section already exists
		for(Section section:sections)
		{
			if(section.getHeader().equals(newSection.getHeader()))
			{
				throw new ValidatingException(String.format(
						"cannot add section \"%s\". It already exists!",
						section.getHeader().getId()));
			}
		}

		sections.add(newSection);
		validateAllElements();
	}

	/**
	 * Remove section by name.
	 * @param name section name
	 * @return true if section was removed
	 */
	public boolean removeSection(String name)
	{
		boolean removed = false;
		for(Section section:sections)
		{
			if(section.getHeader().getId().equals(name))
			{
				if(!section.isMandatory())
				{
					sections.remove(section);
				}
				else
				{
					throw new ValidatingException(String.format(
							"Cannot remove mandatory section \"%s\"!", name));
				}
				removed = true;
			}
		}

		validateAllElements();

		return removed;
	}

	/**
	 * Loads config from file.
	 * @param path to ini file
	 */
	public void loadINI(String path) throws FileNotFoundException, IOException
	{
		loadINI(new FileInputStream(path));
	}

	/**
	 * Loads config from stream.
	 * @param in input stream
	 */
	public void loadINI(InputStream in) throws IOException
	{
		sections.clear();

		SectionReader input = new SectionReader(new InputStreamReader(in));

		// load all sections
		while (true)
		{
			List<String> section = input.readSection();

			if (section.size() > 0)
			{
				sections.add(new Section(section, this));
			}
			else
			{
				break;
			}
		}

		// validate
		if (schema != null)
		{
			try
			{
				loadDefaults();
			}
			catch (NullPointerException e)
			{
				throw new ValidatingException(
						"XML file functioning like a schema is bad formatted!" +
						" Please validate XML by our dtd schema!");
			}
		}
		else
		{
			if (validity == Validity.Strict)
			{
				throw new ValidatingException(
						"Validity is set to strict, but there is no schema!");
			}
		}

		validateAllElements();
	}

	/**
	 * Saves configuration to file.
	 * @param path - path to resulting configuration file
	 */
	public void saveINI(String path) throws FileNotFoundException, IOException
	{
		saveINI(new FileOutputStream(path));
	}

	/**
	 * Saves configuration to stream.
	 * @param out - output stream
	 */
	public void saveINI(OutputStream out) throws IOException
	{
		BufferedWriter output = new BufferedWriter(new OutputStreamWriter(out));

		for (Section section : sections)
		{
			section.save(output);
		}

		output.close();
	}

	/**
	 * Formats configuration as string for debug purposes.
	 * @return debug configuration as human readable string
	 */
	@Override
	public String toString()
	{
		StringBuilder string = new StringBuilder();

		for (Section section : sections)
		{
			string.append(section.toString());
		}

		return string.toString();
	}

	/**
	 * Removes backslashes except escaped ones.
	 * @param value string with backslashes
	 * @return string with backslashed removed
	 */
	static String cleanFromBackSlashes(String value)
	{
		StringBuilder cleaningBuilder = new StringBuilder(value);

		for (int i = 0; i < cleaningBuilder.length(); i++)
		{
			if (cleaningBuilder.charAt(i) == '\\')
			{
				// don't clean backshlash after backslash, which is correct
				cleaningBuilder.deleteCharAt(i);
			}
		}

		return cleaningBuilder.toString();
	}

	/**
	 * Normalize string by removing blank beginning and ending. Interpret
	 * slashes and omit escaped space removal.
	 * @param text input string
	 * @return trimmed string
	 */
	static String trimBlank(String text)
	{
		// check for escaped space at end
		boolean whiteEnd = false;
		if(text.endsWith("\\ "))
		{
			whiteEnd = true;
		}

		// trim will remove wanted escaped space at end
		// but it will be added later
		text = text.trim();
		text = cleanFromBackSlashes(text);

		// add ending space
		if(whiteEnd)
		{
			text.concat(" ");
		}

		return text;
	}
}//end Configurator

